﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics;
using System.Reflection;
using System.Xml;
using Xom.Attributes;
using Xom.Elements;
using Xom.Utilities;

namespace Xom.Engine
{
    public class XmlDocumentEngine : XomEngine
    {
        /// <summary>
        /// Notificate if the content of the xml file has changed
        /// </summary>
        public delegate void XmlDocumentChangedEventHandler(XmlDocument xmlFile);

        /// <summary>
        /// Notificate if the content of the xml file has changed
        /// </summary>
        public event XmlDocumentChangedEventHandler OnXmlDocumentContentChanged;

        /// <summary>
        /// Creates an object and initalizes it with passed information
        /// </summary>
        /// <param name="source">data soruce holding information to be passed into newly created object</param>
        /// <param name="target">Type to be created</param>
        /// <returns>New object or null if creation failed</returns>
        public object StartMapping(XmlNode source, Type target)
        {
            if (source == null) return null;
            if (target == null) return null;

            IXomElement element = CreateNewXomElement(target) as IXomElement;
            CreateElementBinding(source, element, target);

            return element;
        }

        private void CreateElementBinding(XmlNode source, IXomElement element, Type objectType)
        {
            //get all property informations
            IList<MappedPropertyInfo> properties = GetOrAddTypeProperties(objectType);

            foreach (MappedPropertyInfo propertyInfo in properties)
            {
                // if no xml name is set, use the default content
                XmlNode valueNode = source;
                if (!string.IsNullOrEmpty(propertyInfo.Binding.XmlName))
                {
                    valueNode = XmlUtility.GetElementByName(source, propertyInfo.Binding, false);
                }

                //can't find property in xml
                if (valueNode == null)
                {
                    if (propertyInfo.Binding.IsRequired) Trace.TraceWarning("Can't find xml element " + propertyInfo.Binding.XmlName + " in " + source.Name);
                    //add property mapping with empty node reference
                    element.Properties.Add(propertyInfo.Info.Name, new XomPropertyInfo<XmlNode>(propertyInfo.Info, new XomNode<XmlNode>(null)));
                    continue;
                }

                string assingableValue = String.IsNullOrEmpty(valueNode.Value) ? valueNode.InnerText : valueNode.Value;

                //complex property (list or special object)
                if (TypeUtility.IsSimpleType(propertyInfo.Info.PropertyType))
                //old verions:if (propertyInfo.Binding.ObjectType != null)
                {
                    propertyInfo.Info.SetValue(element, ConvertXmlValueToObject(assingableValue, propertyInfo.Info.PropertyType), null);
                }
                else
                {
                    if (TypeUtility.IsListType(propertyInfo.Info.PropertyType))
                    //old version:if (propertyInfo.Info.PropertyType.IsGenericType)
                    {
                        CreateElementListBinding(valueNode.ParentNode, element, propertyInfo);
                    }
                    else
                    {
                        Type elementType = propertyInfo.Binding.ObjectType;
                        if (elementType == null) elementType = propertyInfo.Info.PropertyType;

                        IXomElement newElement = CreateNewXomElement(elementType) as IXomElement;
                        CreateElementBinding(valueNode, newElement, elementType);

                        propertyInfo.Info.SetValue(element, newElement, null);
                    }
                }

                //add property mapping
                element.Properties.Add(propertyInfo.Info.Name, new XomPropertyInfo<XmlNode>(propertyInfo.Info, new XomNode<XmlNode>(valueNode)));
            }

            //register events
            element.PropertyChanged += OnXomElementPropertyChanged;
            element.RootNode =  new XomNode<XmlNode>(source);
            element.XomCollectionChanged += OnXomCollectionChanged;

            //..
        }

        private void OnXomCollectionChanged(IXomElement sender, string propertyName, NotifyCollectionChangedEventArgs notifyCollectionChangedEventArgs)
        {
            switch (notifyCollectionChangedEventArgs.Action)
            {
                case NotifyCollectionChangedAction.Add:
                    {
                        AddXomElementToList(sender, propertyName, notifyCollectionChangedEventArgs.NewItems[0] as IXomElement, notifyCollectionChangedEventArgs.NewStartingIndex);
                        break;
                    }
                case NotifyCollectionChangedAction.Remove:
                    {
                        RemoveXomElementFromList(sender, notifyCollectionChangedEventArgs.OldItems[0] as IXomElement);
                        break;
                    }
            }
        }

        private void AddXomElementToList(IXomElement parent, string propertyName, IXomElement element, int index)
        {
            if (!parent.Properties.ContainsKey(propertyName)) return;
            IList parentList = parent.Properties[propertyName].Info.GetValue(parent, null) as IList;
            if (parentList == null) return;

            XomBinding binding = GetPropertyBinding(parent.GetType(), propertyName);
            XmlNode newXmlNode = CreateNewXmlElementFromObject(element, ((XomNode<XmlNode>)parent.RootNode).Value.OwnerDocument, binding.XmlName);
            if (newXmlNode == null) return;

            if (index >= parentList.Count-1)
            {
                ((XomNode<XmlNode>)parent.RootNode).Value.AppendChild(newXmlNode);
            }
            else
            {
                IXomElement refElement = parentList[index + 1] as IXomElement;
                if (refElement == null || refElement.RootNode == null) return;

                ((XomNode<XmlNode>)parent.RootNode).Value.InsertBefore(newXmlNode, ((XomNode<XmlNode>)refElement.RootNode).Value);
            }

            RaiseXmlContentChanged(((XomNode<XmlNode>)parent.RootNode).Value.OwnerDocument);
        }

        private void RemoveXomElementFromList(IXomElement parent, IXomElement element)
        {
            if (element == null || element.RootNode == null) return;
            if (parent == null || parent.RootNode == null) return;

            ((XomNode<XmlNode>)parent.RootNode).Value.RemoveChild(((XomNode<XmlNode>)element.RootNode).Value);

            RaiseXmlContentChanged(((XomNode<XmlNode>)parent.RootNode).Value.OwnerDocument);
        }

        private void OnXomElementPropertyChanged(object sender, PropertyChangedEventArgs propertyChangedEventArgs)
        {
            IXomElement element = sender as IXomElement;
            if (element == null) return;
            //check property name
            if (!element.Properties.ContainsKey(propertyChangedEventArgs.PropertyName)) return;

            //get the new value
            object propertyValue = element.Properties[propertyChangedEventArgs.PropertyName].Info.GetValue(sender, null);
            XmlNode propertyNode = ((IXomPropertyInfo<XmlNode>)element.Properties[propertyChangedEventArgs.PropertyName]).Node.Value;
            
            XomBinding propertyBinding = GetPropertyBinding(sender.GetType(), propertyChangedEventArgs.PropertyName);
            if (propertyBinding == null) return;

            //create a new xml node for the property if no one exists and the value is not null 
            if (propertyNode == null && propertyValue != null)
            {
                //todo create single xml node
                propertyNode = CreateNewXmlElementFromProperty(element, propertyChangedEventArgs.PropertyName, element.Properties[propertyChangedEventArgs.PropertyName].Info ,propertyValue); 
            }

            //if propertyNode is still null and its a "content" property than set the node from parent
            if (propertyNode == null)
            {
                if (!string.IsNullOrEmpty(propertyBinding.XmlName)) return;
                propertyNode = ((XomNode<XmlNode>)element.RootNode).Value;
            }

            //remove xml nodes if the new value is null
            if (propertyValue == null || string.IsNullOrEmpty(propertyValue.ToString()))
            {
                //is the element required?
                if (propertyBinding.IsRequired)
                {
                    propertyNode.InnerText = string.Empty;
                    RaiseXmlContentChanged(((XomNode<XmlNode>)element.RootNode).Value.OwnerDocument);
                    return;
                }

                //don't remove content nodes
                if (string.IsNullOrEmpty(propertyBinding.XmlName)) return;
                if (propertyBinding.IsAttribute)
                {
                    ((XomNode<XmlNode>)element.RootNode).Value.Attributes.Remove(propertyNode as XmlAttribute);
                }
                else
                {
                    ((XomNode<XmlNode>)element.RootNode).Value.RemoveChild(propertyNode);
                }

                //todo check if set to null is needed?
                ((IXomPropertyInfo<XmlNode>)element.Properties[propertyChangedEventArgs.PropertyName]).Node.Value = null;
                RaiseXmlContentChanged(((XomNode<XmlNode>)element.RootNode).Value.OwnerDocument);
            }
            //else set new value
            else
            {
                if (propertyValue is IXomElement)
                {
                    XmlNode newPropertyNode = CreateNewXmlElementFromObject(propertyValue as IXomElement, ((XomNode<XmlNode>)element.RootNode).Value.OwnerDocument, propertyBinding.XmlName);
                    ((XomNode<XmlNode>)element.RootNode).Value.ReplaceChild(newPropertyNode, propertyNode);
                }
                else
                {
                    propertyNode.InnerText = ConvertObjectValueToString(propertyValue);
                }

                RaiseXmlContentChanged(((XomNode<XmlNode>)element.RootNode).Value.OwnerDocument);
            }
        }

        private void RaiseXmlContentChanged(XmlDocument xmlFile)
        {
            if (OnXmlDocumentContentChanged != null) OnXmlDocumentContentChanged(xmlFile);
        }

        private void CreateElementListBinding(XmlNode source, IXomElement parent, MappedPropertyInfo propertyInfo)
        {
            //try to cast property to IList
            IList list = (IList)propertyInfo.Info.GetValue(parent, null);
            if (list == null) return;

            //check generic type
            Type childType = propertyInfo.Binding.ObjectType;
            if (childType == null) childType = TypeUtility.GetListType(propertyInfo.Info.PropertyType);
            bool isSimpleListType = TypeUtility.IsSimpleType(childType);

            foreach (XmlNode child in source.ChildNodes)
            {
                if (child.Name != propertyInfo.Binding.XmlName) continue;

                object newChildElement;

                // simple list with primitive types
                if (isSimpleListType)
                {
                    newChildElement = ConvertXmlValueToObject(child.Value, childType);
                }
                else
                {
                    newChildElement = CreateNewXomElement(propertyInfo.Binding.ObjectType);
                    CreateElementBinding(child, newChildElement as IXomElement, propertyInfo.Binding.ObjectType);
                }

                list.Add(newChildElement);
            }
        }

        private XmlNode CreateNewXmlElementFromProperty(IXomElement element, string propertyName, PropertyInfo info, object objectValue)
        {
            if (element == null) return null;
            if (string.IsNullOrEmpty(propertyName)) return null;

            XmlDocument document = ((XomNode<XmlNode>)element.RootNode).Value.OwnerDocument;
            XomBinding binding = GetPropertyBinding(element.GetType(), propertyName);

            //if context element than exit
            if (string.IsNullOrEmpty(binding.XmlName)) return null;

            XmlNode newPropertyNode = null;

            if (binding.IsAttribute)
            {
                newPropertyNode = document.CreateAttribute(binding.XmlName);
                //get prevoius xml node
                XmlAttribute prevNode = GetPreviousXmlAttribute(((XomNode<XmlNode>)element.RootNode).Value, element.GetType(), binding);
                if (prevNode == null)
                {
                    ((XomNode<XmlNode>)element.RootNode).Value.Attributes.Append(newPropertyNode as XmlAttribute);
                }
                else
                {
                    ((XomNode<XmlNode>)element.RootNode).Value.Attributes.InsertBefore(newPropertyNode as XmlAttribute, prevNode);
                }
            }
            else
            {
                if (binding.ObjectType == null)
                {
                    newPropertyNode = CreateNewXmlElementFromObject(element, document, binding.XmlName);
                }
                else
                {
                    newPropertyNode = document.CreateNode("element", binding.XmlName, null);
                    //get prevoius xml node
                    XmlNode prevNode = GetPreviousXmlNode(((XomNode<XmlNode>)element.RootNode).Value, element.GetType(), binding);
                    if (prevNode == null)
                    {
                        ((XomNode<XmlNode>)element.RootNode).Value.AppendChild(newPropertyNode);
                    }
                    else
                    {
                        ((XomNode<XmlNode>)element.RootNode).Value.InsertBefore(newPropertyNode, prevNode);
                    }
                }
            }

            RaiseXmlContentChanged(((XomNode<XmlNode>)element.RootNode).Value.OwnerDocument);

            return newPropertyNode;
        }

        private XmlNode CreateNewXmlElementFromObject(IXomElement element, XmlDocument document, string xmlName)
        {
            if (element == null) return null;
            if (document == null) return null;
            if (string.IsNullOrEmpty(xmlName)) return null;

            XmlNode newXmlNode = document.CreateNode("element", xmlName, "");
            IList<MappedPropertyInfo> propertyInfos = GetOrAddTypeProperties(element.GetType());

            foreach (MappedPropertyInfo property in propertyInfos)
            {

                object propertyValue = property.Info.GetValue(element, null);
                string propertyStringValue = null;
                if (propertyValue != null) propertyStringValue = propertyValue.ToString();

                // skip property if it's empty and not required
                if (!property.Binding.IsRequired && string.IsNullOrEmpty(propertyStringValue))
                {
                    element.Properties.Add(property.Info.Name, new XomPropertyInfo<XmlNode>(property.Info, new XomNode<XmlNode>(null)));
                    continue;
                }

                XmlNode newPropertyNode = null;

                if (property.Binding.IsAttribute)
                {
                    newPropertyNode = document.CreateAttribute(property.Binding.XmlName);
                    newPropertyNode.Value = ConvertObjectValueToString(propertyValue);
                    newXmlNode.Attributes.Append(newPropertyNode as XmlAttribute);
                }
                else
                {
                    //content
                    if (string.IsNullOrEmpty(property.Binding.XmlName))
                    {
                        newXmlNode.InnerText = ConvertObjectValueToString(propertyValue);
                        newPropertyNode = newXmlNode;
                    }
                    else
                    {
                        if (property.Binding.ObjectType != null)
                        {
                            newPropertyNode = CreateNewXmlElementFromObject(propertyValue as IXomElement, document, property.Binding.XmlName);
                            if (newPropertyNode == null) continue;
                        }
                        else
                        {
                            newPropertyNode = document.CreateNode("element", property.Binding.XmlName, null);
                            if (!string.IsNullOrEmpty(propertyStringValue)) newPropertyNode.InnerText = ConvertObjectValueToString(propertyValue);
                        }

                        newXmlNode.AppendChild(newPropertyNode);
                    }
                }

                element.Properties.Add(property.Info.Name, new XomPropertyInfo<XmlNode>(property.Info, new XomNode<XmlNode>(newPropertyNode)));
            }

            //register events
            element.PropertyChanged += OnXomElementPropertyChanged;
            element.RootNode = new XomNode<XmlNode>(newXmlNode);
            element.XomCollectionChanged += OnXomCollectionChanged;

            return newXmlNode;
        }

        private XmlAttribute GetPreviousXmlAttribute(XmlNode parent, Type objectType, XomBinding binding)
        {
            if (parent.Attributes == null) return null;
            if (parent.Attributes.Count == 0) return null;

            IList<Tuple<int, XmlAttribute>> indexNodeList = new List<Tuple<int, XmlAttribute>>();

            //get for all existing xml elements the mapped sewuence index
            foreach (XmlAttribute child in parent.Attributes)
            {
                int index = GetXmlMappingIndexByName(objectType, child.Name);
                if (index == -1) continue;

                indexNodeList.Add(new Tuple<int, XmlAttribute>(index, child));
            }

            int elementIndex = GetXmlMappingIndexByName(objectType, binding.XmlName);

            //get get previouse existing index
            foreach (Tuple<int, XmlAttribute> indexNode in indexNodeList)
            {
                if (indexNode.Item1 > elementIndex)
                {
                    return indexNode.Item2;
                }
            }

            return null;
        }

        private XmlNode GetPreviousXmlNode(XmlNode parent, Type objectType, XomBinding binding)
        {
            if (parent.ChildNodes.Count == 0) return null;

            IList<Tuple<int, XmlNode>> indexNodeList = new List<Tuple<int, XmlNode>>();

            //get for all existing xml elements the mapped sewuence index
            foreach (XmlNode child in parent.ChildNodes)
            {
                int index = GetXmlMappingIndexByName(objectType, child.Name);
                if (index == -1) continue;

                indexNodeList.Add(new Tuple<int, XmlNode>(index, child));
            }

            int elementIndex = GetXmlMappingIndexByName(objectType, binding.XmlName);

            //get get previouse existing index
            foreach (Tuple<int, XmlNode> indexNode in indexNodeList)
            {
                if (indexNode.Item1 > elementIndex)
                {
                    return indexNode.Item2;
                }
            }

            return null;
        }

        private int GetXmlMappingIndexByName(Type objectType, string elementName)
        {
            if (objectType == null) return -1;
            if (string.IsNullOrEmpty(elementName)) return -1;

            if (!_typeProperties.ContainsKey(objectType)) return -1;

            for (int i = 0; i < _typeProperties[objectType].Count; i++)
            {
                if (_typeProperties[objectType][i].Binding.XmlName == elementName)
                {
                    return i;
                }
            }

            return -1;
        }
    }
}
